package zhongbo.javaweb.instagram.web.controller;

import com.mongodb.client.gridfs.GridFSFindIterable;
import com.mongodb.client.gridfs.model.GridFSFile;
import com.mongodb.client.result.UpdateResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.bson.BsonString;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.gridfs.GridFsResource;
import org.springframework.data.mongodb.gridfs.GridFsTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import zhongbo.javaweb.instagram.dto.JsonRespDto;
import zhongbo.javaweb.instagram.util.JsonRespUtil;
import zhongbo.javaweb.instagram.web.util.Md5Util;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.*;

/**
 * 说明：
 * 使用MongoDb GridFs作为文件对象存储
 *
 * @author zhongboWork
 * @version 1.0
 * @date 2022-05-10 14:19
 */
@Controller
@RequestMapping("Mongo/file")
@Api(value = "Mongo文件相关接口", tags = {"Mongo文件相关接口"})
@Slf4j
public class MongoFileController {

    @Autowired
    private GridFsTemplate gridFsTemplate;

    @ResponseBody
    @RequestMapping(value = "/add", method = RequestMethod.POST, headers = "content-type=multipart/form-data")
    @ApiOperation(value = "添加一个文件接口")
    public JsonRespDto<UpdateResult> addOrUpdateOne(
            @ApiParam(required = true, value = "文件") @RequestParam("file") MultipartFile file,
            @ApiParam(required = true, value = "文件描述") @RequestParam(value = "desc") String desc
    ) throws IOException {
        if (file == null) {
            throw new RuntimeException("未上传文件");
        }
        InputStream inputStream = file.getInputStream();
        int available = inputStream.available();
        if (available <= 0) {
            throw new RuntimeException("禁止上传空文件");
        }
        byte[] bytes = file.getBytes();
        String originalFilename = file.getOriginalFilename();
        String contentType = file.getContentType();
        String md5 = Md5Util.md5(new ByteArrayInputStream(bytes));
        Document document = new Document();
        document.put("fileName", originalFilename);
        document.put("size", available);
        document.put("contentType", contentType);
        document.put("md5", md5);
        document.put("desc", desc);
        document.put("createTime", new Date().toString());
        Criteria condition = Criteria.where("md5").is(md5);
        GridFSFile one = gridFsTemplate.findOne(Query.query(condition));
        if (one != null) {
            String findId = one.getObjectId().toString();
            log.info("已上传的文件, md5: {}, id: {}", md5, findId);
            BsonString bsonString = new BsonString(findId);
            UpdateResult updateResult = UpdateResult.acknowledged(1, 1L, bsonString);
            return new JsonRespDto<UpdateResult>().setData(updateResult).makeSuccess();
        }
        ObjectId store = gridFsTemplate.store(new ByteArrayInputStream(bytes), originalFilename, contentType, document);
        String id = store.toString();
        BsonString bsonString = new BsonString(id);
        UpdateResult updateResult = UpdateResult.acknowledged(1, 1L, bsonString);
        return new JsonRespDto<UpdateResult>().setData(updateResult).makeSuccess();
    }

    @ResponseBody
    @RequestMapping(value = "/del/{id}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ApiOperation(value = "删除一个文件接口")
    public JsonRespDto<String> delete(
            @PathVariable(MongoPostController.MongoKey.primaryId) String id
    ) {
        if (StringUtils.isEmpty(id)) {
            return new JsonRespDto<String>().makeError().appendMsg("未指定id");
        }
        gridFsTemplate.delete(Query.query(Criteria.where("_id").is(id)));
        log.info("删除文件,id={}", id);
        return new JsonRespDto<String>().setData(id).makeSuccess();
    }

    @ResponseBody
    @RequestMapping(value = "/del/md5/{md5}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ApiOperation(value = "删除一个文件MD5接口")
    public JsonRespDto<String> deleteByMd5(
            @PathVariable(MongoPostController.MongoKey.md5) String md5
    ) {
        if (StringUtils.isEmpty(md5)) {
            return new JsonRespDto<String>().makeError().appendMsg("未指定md5");
        }
        gridFsTemplate.delete(Query.query(Criteria.where("md5").is(md5)));
        log.info("删除文件,md5={}", md5);
        return new JsonRespDto<String>().setData(md5).makeSuccess();
    }

    @ResponseBody
    @RequestMapping(value = "/delete", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ApiOperation(value = "删除多个文件接口")
    public JsonRespDto<List<String>> deleteList(
            @RequestBody List<String> ids
    ) {
        if (ids.isEmpty()) {
            return new JsonRespDto<List<String>>().makeError().appendMsg("未指定id");
        }
        List<String> deletedList = new ArrayList<>();
        for (String id : ids) {
            JsonRespDto<String> delete = delete(id);
            if (!JsonRespUtil.isSuccess(delete)) {
                break;
            }
            deletedList.add(id);
        }
        log.info("删除{}个文件,ids={}", deletedList.size(), ids);
        return new JsonRespDto<List<String>>().setData(deletedList).makeSuccess();
    }

    @ResponseBody
    @RequestMapping(value = "/delete/md5", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ApiOperation(value = "删除多个文件MD5接口")
    public JsonRespDto<List<String>> deleteListByMd5(
            @RequestBody List<String> md5List
    ) {
        if (md5List.isEmpty()) {
            return new JsonRespDto<List<String>>().makeError().appendMsg("未指定md5列表");
        }
        List<String> deletedList = new ArrayList<>();
        for (String md5 : md5List) {
            JsonRespDto<String> delete = deleteByMd5(md5);
            if (!JsonRespUtil.isSuccess(delete)) {
                break;
            }
            deletedList.add(md5);
        }
        log.info("删除{}个文件,md5列表={}", deletedList.size(), md5List);
        return new JsonRespDto<List<String>>().setData(deletedList).makeSuccess();
    }

    @ResponseBody
    @RequestMapping(value = "/find/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ApiOperation(value = "查询一个文件接口")
    public JsonRespDto<Document> findOneById(
            @PathVariable(MongoPostController.MongoKey.primaryId) String id
    ) {
        if (StringUtils.isEmpty(id)) {
            return new JsonRespDto<Document>().makeError().appendMsg("未指定id");
        }
        GridFSFile gridFsFile = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(id)));
        Document document = null;
        if (gridFsFile != null) {
            document = transFormGridFsFile(gridFsFile);
        }
        log.info("查询一个文件: {}, id: {}", document, id);
        return new JsonRespDto<Document>().setData(document).makeSuccess();
    }

    @ResponseBody
    @RequestMapping(value = "/find/md5/{md5}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ApiOperation(value = "查询一个文件接口")
    public JsonRespDto<Document> findOneByMd5(
            @PathVariable(MongoPostController.MongoKey.md5) String md5
    ) {
        if (StringUtils.isEmpty(md5)) {
            return new JsonRespDto<Document>().makeError().appendMsg("未指定md5");
        }
        GridFSFile gridFsFile = gridFsTemplate.findOne(Query.query(Criteria.where("md5").is(md5)));
        Document document = null;
        if (gridFsFile != null) {
            document = transFormGridFsFile(gridFsFile);
        }
        log.info("查询一个文件md5: {}, md5: {}", document, md5);
        return new JsonRespDto<Document>().setData(document).makeSuccess();
    }

    @ResponseBody
    @RequestMapping(value = "/page", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ApiOperation(value = "分页条件查询文件接口")
    public JsonRespDto<List<Document>> findPage(
            @ApiParam(required = true, value = "查询数量(1~100)") int pageSize,
            @ApiParam(required = true, value = "查询页数(1~n)") int pageNumber
    ) {
        if (pageSize < 1) {
            pageSize = 1;
        }
        int maxPageSize = 100;
        if (pageSize > maxPageSize) {
            pageSize = maxPageSize;
        }
        int start = pageNumber > 0 ? (pageNumber - 1) * pageSize : 0;
        Query query = new Query().limit(pageSize).skip(start);
        GridFSFindIterable gridFsFiles = gridFsTemplate.find(query);
        gridFsFiles.limit(pageSize).skip(start);
        List<Document> fsFileList = transFormGridFsFiles(gridFsFiles);
        return new JsonRespDto<List<Document>>().setData(fsFileList).makeSuccess();
    }

    @ResponseBody
    @RequestMapping(value = "/page/{start}/{size}", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ApiOperation(value = "条件查询文件接口")
    public JsonRespDto<List<Document>> findPage(
            @ApiParam(required = true, value = "文档") @RequestBody Document document,
            @ApiParam(required = true, value = "位置,从1开始") @PathVariable(MongoPostController.MongoKey.limit) int start,
            @ApiParam(required = true, value = "数量") @PathVariable(MongoPostController.MongoKey.skip) int size
    ) {
        if (size < 1) {
            size = 1;
        }
        int maxSize = 100;
        if (size > maxSize) {
            size = maxSize;
        }
        start--;
        if (start < 0) {
            start = 0;
        }
        Query query = new Query();
        Criteria criteria = new Criteria();
        if (document != null) {
            Set<Map.Entry<String, Object>> entries = document.entrySet();
            if (entries.size() > 0) {
                for (Map.Entry<String, Object> entry : entries) {
                    String key = entry.getKey();
                    Object value = entry.getValue();
                    criteria.and(key).is(value);
                }
                query.addCriteria(criteria);
            }
        }
        query.limit(size).skip(start);
        GridFSFindIterable gridFsFiles = gridFsTemplate.find(query);
        gridFsFiles.limit(size).skip(start);
        List<Document> fsFileList = transFormGridFsFiles(gridFsFiles);
        return new JsonRespDto<List<Document>>().setData(fsFileList).makeSuccess();
    }

    private List<Document> transFormGridFsFiles(GridFSFindIterable gridFsFiles) {
        List<Document> fsFileList = new ArrayList<>();
        for (GridFSFile gridFsFile : gridFsFiles) {
            Document document = transFormGridFsFile(gridFsFile);
            fsFileList.add(document);
        }
        return fsFileList;
    }

    private Document transFormGridFsFile(GridFSFile gridFsFile) {
        Document document = new Document();
        // document.put("_id", gridFsFile.getId());
        document.put("id", gridFsFile.getObjectId().toString());
        document.put("filename", gridFsFile.getFilename());
        document.put("length", gridFsFile.getLength());
        document.put("chunkSize", gridFsFile.getChunkSize());
        document.put("uploadDate", gridFsFile.getUploadDate());
        document.put("md5", gridFsFile.getMD5());
        document.put("metadata", gridFsFile.getMetadata());
        if (gridFsFile.getExtraElements() != null) {
            document.put("extraElements", gridFsFile.getExtraElements());
        }
        return document;
    }

    @RequestMapping(value = "/get/{id}", method = {RequestMethod.GET})
    @ResponseBody
    @ApiOperation(value = "根据id下载文件接口")
    public JsonRespDto<String> fileById(
            HttpServletRequest req
            , HttpServletResponse resp,
            @ApiParam(required = true, value = "文件id") @PathVariable(MongoPostController.MongoKey.primaryId) String id
    ) {
        if (id == null) {
            return new JsonRespDto<String>().makeError().appendMsg("未指定id");
        }
        GridFSFile one = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(id)));
        if (one != null) {
            return doDownLoad(one, resp);
        } else {
            resp.setStatus(404);
        }
        return null;
    }

    @RequestMapping(value = "/get/{id}/**", method = {RequestMethod.GET})
    @ResponseBody
    @ApiOperation(value = "自定义根据id下载文件接口")
    public JsonRespDto<String> fileByIdDown(
            HttpServletRequest req
            , HttpServletResponse resp,
            @ApiParam(required = true, value = "文件id") @PathVariable(MongoPostController.MongoKey.primaryId) String id
    ) {
        return fileById(req, resp, id);
    }

    @RequestMapping(value = "/get/md5/{md5}", method = {RequestMethod.GET})
    @ResponseBody
    @ApiOperation(value = "根据md5下载文件接口")
    public JsonRespDto<String> fileByMd5(
            HttpServletRequest req
            , HttpServletResponse resp,
            @ApiParam(required = true, value = "文件md5") @PathVariable(MongoPostController.MongoKey.md5) String md5
    ) {
        // @see <a href="https://www.yht7.com/news/162781">Spring Boot使用GridFS实现文件的上传和下载方式</a>
        // @see <a href="https://blog.csdn.net/frankcheng5143/article/details/77825491">使用GridFsTemplate在mongodb中存取文件</a>
        if (md5 == null) {
            return new JsonRespDto<String>().makeError().appendMsg("未指定md5");
        }
        GridFSFile one = gridFsTemplate.findOne(Query.query(Criteria.where("md5").is(md5)));
        if (one != null) {
            return doDownLoad(one, resp);
        } else {
            resp.setStatus(404);
        }
        return null;
    }

    @RequestMapping(value = "/get/md5/{md5}/**", method = {RequestMethod.GET})
    @ResponseBody
    @ApiOperation(value = "自定义根据md5下载文件接口")
    public JsonRespDto<String> fileByMd5Down(
            HttpServletRequest req
            , HttpServletResponse resp,
            @ApiParam(required = true, value = "文件md5") @PathVariable(MongoPostController.MongoKey.md5) String md5
    ) {
        return fileByMd5(req, resp, md5);
    }

    private JsonRespDto<String> doDownLoad(GridFSFile one, HttpServletResponse resp) {
        String fileName = one.getFilename();
        String contentTypeKey = "_contentType";
        String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        Document metadata = one.getMetadata();
        if (metadata != null && metadata.get(contentTypeKey) != null) {
            contentType = metadata.getString(contentTypeKey);
        }
        GridFsResource resource = gridFsTemplate.getResource(one);
        ServletOutputStream out = null;
        InputStream in = null;
        try {
            in = resource.getInputStream();
            resp.setContentType(contentType);
            String defaultCharset = Charset.defaultCharset().name();
            resp.setCharacterEncoding(defaultCharset);
            String encode = URLEncoder.encode(fileName, defaultCharset);
            resp.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + encode);
            out = resp.getOutputStream();
            int copy = IOUtils.copy(in, out);
            if (copy <= 0) {
                return new JsonRespDto<String>().makeError().appendMsg("传输文件长度错误!size: " + copy);
            }
        } catch (IOException e) {
            log.error("下载文件失败!", e);
            throw new RuntimeException("下载文件失败!", e);
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
                if (out != null) {
                    out.close();
                }
            } catch (IOException e) {
                log.error("下载文件异常,关闭流失败!", e);
            }
        }
        return null;
    }
}
